/** * The contents of this file are subject to the OpenMRS Public License * Version 1.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://license.openmrs.org * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * Copyright (C) OpenMRS, LLC. All Rights Reserved. */ package org.openmrs.module.sync.api.db.hibernate; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.io.Reader; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.sql.Blob; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.TreeSet; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.Criteria; import org.hibernate.FlushMode; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.criterion.Expression; import org.hibernate.criterion.MatchMode; import org.hibernate.criterion.Order; import org.hibernate.criterion.Projections; import org.hibernate.criterion.Restrictions; import org.hibernate.engine.SessionFactoryImplementor; import org.hibernate.exception.ConstraintViolationException; import org.hibernate.impl.CriteriaImpl; import org.hibernate.impl.SessionImpl; import org.hibernate.loader.OuterJoinLoader; import org.hibernate.loader.criteria.CriteriaLoader; import org.hibernate.persister.entity.OuterJoinLoadable; import org.openmrs.GlobalProperty; import org.openmrs.OpenmrsObject; import org.openmrs.api.context.Context; import org.openmrs.api.db.DAOException; import org.openmrs.module.sync.SyncClass; import org.openmrs.module.sync.SyncConstants; import org.openmrs.module.sync.SyncSubclassStub; import org.openmrs.module.sync.SyncRecord; import org.openmrs.module.sync.SyncRecordState; import org.openmrs.module.sync.SyncStatistic; import org.openmrs.module.sync.SyncUtil; import org.openmrs.module.sync.api.db.SyncDAO; import org.openmrs.module.sync.ingest.SyncImportRecord; import org.openmrs.module.sync.ingest.SyncIngestException; import org.openmrs.module.sync.server.RemoteServer; import org.openmrs.module.sync.server.RemoteServerType; import org.openmrs.module.sync.server.SyncServerRecord; import org.openmrs.util.OpenmrsConstants; import org.openmrs.util.OpenmrsUtil; import org.springframework.util.StringUtils; import org.w3c.dom.Element; import org.w3c.dom.NodeList; public class HibernateSyncDAO implements SyncDAO { protected final Log log = LogFactory.getLog(getClass()); /** * Hibernate session factory */ private SessionFactory sessionFactory; public HibernateSyncDAO() { } /** * Set session Factory interceptor * * @param sessionFactory */ public void setSessionFactory(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } /** * @see org.openmrs.module.sync.api.db.SyncDAO#createSyncRecord(org.openmrs.module.sync.SyncRecord) */ public void createSyncRecord(SyncRecord record) throws DAOException { if (record.getUuid() == null) { //TODO: Create Uuid if missing? throw new DAOException("SyncRecord must have a UUID"); } Session session = sessionFactory.getCurrentSession(); try { session.save(record); } catch (ConstraintViolationException e) { sessionFactory.getCurrentSession().clear(); SyncRecord existingRecord = getSyncRecord(record.getUuid()); if (existingRecord != null) { //compare the contents if (existingRecord.equals(record)) { //this was an identical record getting re-saved, ignore the exception return; } } throw e; } } /** * @see org.openmrs.module.sync.api.db.SyncDAO#updateSyncRecord(org.openmrs.module.sync.SyncRecord) */ public void updateSyncRecord(SyncRecord record) throws DAOException { Session session = sessionFactory.getCurrentSession(); session.saveOrUpdate(record); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#deleteSyncRecord(org.openmrs.module.sync.SyncRecord) */ public void deleteSyncRecord(SyncRecord record) throws DAOException { Session session = sessionFactory.getCurrentSession(); session.delete(record); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#createSyncImportRecord(org.openmrs.module.sync.engine.SyncImportRecord) */ public void createSyncImportRecord(SyncImportRecord record) throws DAOException { if (record.getUuid() == null) { //TODO: Create Uuid if missing? throw new DAOException("SyncImportRecord must have a UUID"); } Session session = sessionFactory.getCurrentSession(); session.save(record); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#updateSyncImportRecord(org.openmrs.module.sync.engine.SyncImportRecord) */ public void updateSyncImportRecord(SyncImportRecord record) throws DAOException { Session session = sessionFactory.getCurrentSession(); session.merge(record); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#deleteSyncImportRecord(org.openmrs.module.sync.engine.SyncImportRecord) */ public void deleteSyncImportRecord(SyncImportRecord record) throws DAOException { Session session = sessionFactory.getCurrentSession(); session.delete(record); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#deleteSyncImportRecordsByServer(java.lang.Integer) */ public void deleteSyncImportRecordsByServer(Integer serverId) throws DAOException { Session session = sessionFactory.getCurrentSession(); session.createSQLQuery("delete from sync_import where source_server_id =:serverId").setInteger("serverId",serverId).executeUpdate(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getNextSyncRecord() */ @SuppressWarnings("unchecked") public SyncRecord getFirstSyncRecordInQueue() throws DAOException { List<SyncRecord> result = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class) .add(Restrictions.in("state", new SyncRecordState[] { SyncRecordState.NEW, SyncRecordState.PENDING_SEND })) .addOrder(Order.asc("timestamp")).addOrder(Order.asc("recordId")).setFetchSize(1).list(); if (result.size() < 1) { return null; } else { return result.get(0); } } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getNextSyncRecord() */ @SuppressWarnings("unchecked") public SyncRecord getLatestRecord() throws DAOException { List<Integer> result = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class) .setProjection(Projections.max("recordId")).list(); if (result.size() < 1) { return null; } else { Integer maxRecordId = result.get(0); return getSyncRecord(maxRecordId); } } @SuppressWarnings("unchecked") public SyncRecord getEarliestRecord(Date afterDate) throws DAOException { Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class) .setProjection(Projections.min("recordId")); if (afterDate != null) criteria.add(Restrictions.ge("timestamp", afterDate)); List<Integer> result = criteria.list(); if (result.size() < 1) { return null; } else { Integer minRecordId = result.get(0); return getSyncRecord(minRecordId); } } /** * @see org.openmrs.module.sync.api.SyncService#getNextRecord(SyncRecord) */ public SyncRecord getNextRecord(SyncRecord record) { Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class); criteria.setProjection(Projections.min("recordId")); criteria.add(Restrictions.gt("recordId", record.getRecordId())); Object nextRecordId = criteria.uniqueResult(); if (nextRecordId == null) { return null; } return getSyncRecord((Integer)nextRecordId); } /** * @see org.openmrs.module.sync.api.SyncService#getPreviousRecord(SyncRecord) */ public SyncRecord getPreviousRecord(SyncRecord record) { Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class); criteria.setProjection(Projections.max("recordId")); criteria.add(Restrictions.lt("recordId", record.getRecordId())); Object prevRecordId = criteria.uniqueResult(); if (prevRecordId == null) { return null; } return getSyncRecord((Integer)prevRecordId); } @SuppressWarnings("unchecked") public List<SyncRecord> getSyncRecords(String query) throws DAOException { return sessionFactory .getCurrentSession() .createCriteria(SyncRecord.class) .add( Expression.or(Restrictions.like("items", query, MatchMode.ANYWHERE), Restrictions.eq("originalUuid", query))).addOrder(Order.desc("timestamp")).setMaxResults(250) // max number of records returned .list(); } public SyncRecord getSyncRecord(Integer recordId) throws DAOException { return (SyncRecord) sessionFactory.getCurrentSession().createCriteria(SyncRecord.class) .add(Restrictions.eq("recordId", recordId)).uniqueResult(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncRecord(java.lang.String) */ public SyncRecord getSyncRecord(String uuid) throws DAOException { return (SyncRecord) sessionFactory.getCurrentSession().createCriteria(SyncRecord.class) .add(Restrictions.eq("uuid", uuid)).uniqueResult(); } public SyncRecord getSyncRecordByOriginalUuid(String originalUuid) throws DAOException { return (SyncRecord) sessionFactory.getCurrentSession().createCriteria(SyncRecord.class) .add(Restrictions.eq("originalUuid", originalUuid)).uniqueResult(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncImportRecord(java.lang.String) */ public SyncImportRecord getSyncImportRecord(String uuid) throws DAOException { return (SyncImportRecord) sessionFactory.getCurrentSession().createCriteria(SyncImportRecord.class) .add(Restrictions.eq("uuid", uuid)).uniqueResult(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncImportRecords(org.openmrs.module.sync.engine.SyncRecordState) */ @SuppressWarnings("unchecked") public List<SyncImportRecord> getSyncImportRecords(SyncRecordState... state) throws DAOException { return sessionFactory.getCurrentSession().createCriteria(SyncImportRecord.class) .add(Restrictions.in("state", state)).addOrder(Order.asc("timestamp")).addOrder(Order.asc("importId")) .list(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncRecords() */ @SuppressWarnings("unchecked") public List<SyncRecord> getSyncRecords() throws DAOException { return sessionFactory.getCurrentSession().createCriteria(SyncRecord.class).addOrder(Order.asc("timestamp")) .addOrder(Order.asc("recordId")).list(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncRecords(org.openmrs.module.sync.engine.SyncRecordState) */ @SuppressWarnings("unchecked") public List<SyncRecord> getSyncRecords(SyncRecordState state) throws DAOException { return sessionFactory.getCurrentSession().createCriteria(SyncRecord.class).add(Restrictions.eq("state", state)) .addOrder(Order.asc("timestamp")).addOrder(Order.asc("recordId")).list(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncRecords(org.openmrs.module.sync.SyncRecordState[], * boolean, java.lang.Integer, org.openmrs.module.sync.server.RemoteServer, java.lang.Integer) */ @SuppressWarnings("unchecked") public List<SyncRecord> getSyncRecords(SyncRecordState[] states, boolean inverse, Integer maxSyncRecords, RemoteServer server, Integer firstRecordId) throws DAOException { if (maxSyncRecords == null) { maxSyncRecords = Integer.parseInt(SyncConstants.PROPERTY_NAME_MAX_RECORDS_DEFAULT); } Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class, "s"); String column = "s.state"; if (server != null) { criteria = criteria.createCriteria("serverRecords", "sr"); criteria.add(Restrictions.eq("sr.syncServer", server)); column = "sr.state"; } if (inverse) criteria.add(Restrictions.not(Restrictions.in(column, states))); else criteria.add(Restrictions.in(column, states)); if (firstRecordId != null) criteria.add(Restrictions.ge("s.recordId", firstRecordId)); criteria.addOrder(Order.asc("s.timestamp")); criteria.addOrder(Order.asc("s.recordId")); // if the user sets -1 as the max records, don't restrict the number of records downloaded/transferred if (maxSyncRecords > 0) criteria.setMaxResults(maxSyncRecords); return criteria.list(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#deleteSyncRecords(org.openmrs.module.sync.SyncRecordState[], * java.util.Date) */ public Integer deleteSyncRecords(SyncRecordState[] states, Date to) throws DAOException { List<String> stateStrings = new ArrayList<String>(); for (SyncRecordState s : states) { stateStrings.add(s.name()); } // no matter what kind of server this current server is (parent or child), the sync server records // must be either committed or not syncing in order to delete them String[] syncServerStates = new String[] { SyncRecordState.NOT_SUPPOSED_TO_SYNC.name(), SyncRecordState.COMMITTED.name() }; // delete all rows in sync_server_id that are of the right state and are old Query deleteSSRQuery = sessionFactory .getCurrentSession() .createSQLQuery( "delete from sync_server_record where state in (:states) and (select timestamp from sync_record sr where sr.record_id = sync_server_record.record_id) < :to"); deleteSSRQuery.setParameterList("states", syncServerStates); deleteSSRQuery.setDate("to", to); Integer quantityDeleted = deleteSSRQuery.executeUpdate(); // this quantity isn't really used // if a sync_record now has zero sync_record_server rows, then that means all // the rows were deleted in the previous query and so the sync_record can also be deleted Query deleteQuery = sessionFactory .getCurrentSession() .createSQLQuery( "delete from sync_record where (select count(*) from sync_server_record ssr where ssr.record_id = sync_record.record_id) = 0 and sync_record.timestamp <= :to and sync_record.state in (:states)"); deleteQuery.setDate("to", to); deleteQuery.setParameterList("states", stateStrings); quantityDeleted = deleteQuery.executeUpdate(); return quantityDeleted; } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncRecords(java.util.Date, java.util.Date, * Integer, Integer) */ @SuppressWarnings("unchecked") public List<SyncRecord> getSyncRecords(Date from, Date to, Integer firstRecordId, Integer numberToReturn, boolean oldestToNewest) throws DAOException { Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class); if (from != null) criteria.add(Restrictions.gt("timestamp", from)); // greater than if (to != null) criteria.add(Restrictions.le("timestamp", to)); // less-than or equal if (numberToReturn != null) criteria.setMaxResults(numberToReturn); if (oldestToNewest) { if (firstRecordId != null) criteria.add(Restrictions.ge("recordId", firstRecordId)); criteria.addOrder(Order.asc("timestamp")); criteria.addOrder(Order.asc("recordId")); } else { if (firstRecordId != null) criteria.add(Restrictions.le("recordId", firstRecordId)); criteria.addOrder(Order.desc("timestamp")); criteria.addOrder(Order.desc("recordId")); } return criteria.list(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getGlobalProperty(String propertyName) */ public String getGlobalProperty(String propertyName) throws DAOException { if (propertyName == null) throw new DAOException("Cannot retrieve property with null property name."); GlobalProperty gp = (GlobalProperty) sessionFactory.getCurrentSession().get(GlobalProperty.class, propertyName); if (gp == null) return null; return gp.getPropertyValue(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#setGlobalProperty(String propertyName, String * propertyValue) */ public void setGlobalProperty(String propertyName, String propertyValue) throws DAOException { if (propertyName == null) throw new DAOException("Cannot set property with null property name."); Session session = sessionFactory.getCurrentSession(); // try to look up the global property first so we use the same uuid for the gp GlobalProperty gp = (GlobalProperty) session.get(GlobalProperty.class, propertyName); if (gp == null) { // the gp doesn't exist, create a new one with a new uuid now gp = new GlobalProperty(propertyName, propertyValue); gp.setUuid(SyncUtil.generateUuid()); } else { gp.setPropertyValue(propertyValue); } session.merge(gp); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#saveRemoteServer(org.openmrs.module.sync.engine.RemoteServer) */ public RemoteServer saveRemoteServer(RemoteServer server) throws DAOException { Session session = sessionFactory.getCurrentSession(); return (RemoteServer) session.merge(server); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#deleteRemoteServer(org.openmrs.module.sync.engine.RemoteServer) */ public void deleteRemoteServer(RemoteServer server) throws DAOException { Session session = sessionFactory.getCurrentSession(); deleteSyncImportRecordsByServer(server.getServerId()); // trying to speed up the deletion process... session.createSQLQuery("delete from sync_server_record where server_id =:serverId").setInteger("serverId", server.getServerId()).executeUpdate(); session.delete(server); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getGlobalProperty(String propertyName) */ public RemoteServer getRemoteServer(Integer serverId) throws DAOException { return (RemoteServer) sessionFactory.getCurrentSession().get(RemoteServer.class, serverId); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getGlobalProperty(String propertyName) */ public RemoteServer getRemoteServer(String uuid) throws DAOException { return (RemoteServer) sessionFactory.getCurrentSession().createCriteria(RemoteServer.class) .add(Restrictions.eq("uuid", uuid)).uniqueResult(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getGlobalProperty(String propertyName) */ public RemoteServer getRemoteServerByUsername(String username) throws DAOException { return (RemoteServer) sessionFactory.getCurrentSession().createCriteria(RemoteServer.class) .add(Restrictions.eq("childUsername", username)).uniqueResult(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getGlobalProperty(String propertyName) */ @SuppressWarnings("unchecked") public List<RemoteServer> getRemoteServers() throws DAOException { return (List<RemoteServer>) sessionFactory.getCurrentSession().createCriteria(RemoteServer.class).list(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getGlobalProperty(String propertyName) */ public RemoteServer getParentServer() throws DAOException { return (RemoteServer) sessionFactory.getCurrentSession().createCriteria(RemoteServer.class) .add(Restrictions.eq("serverType", RemoteServerType.PARENT)).uniqueResult(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#saveSyncClass(org.openmrs.module.sync.SyncClass) */ public void saveSyncClass(SyncClass syncClass) throws DAOException { Session session = sessionFactory.getCurrentSession(); session.saveOrUpdate(syncClass); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#deleteSyncClass(org.openmrs.module.sync.SyncClass) */ public void deleteSyncClass(SyncClass syncClass) throws DAOException { Session session = sessionFactory.getCurrentSession(); session.delete(syncClass); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncClass(Integer) */ public SyncClass getSyncClass(Integer syncClassId) throws DAOException { return (SyncClass) sessionFactory.getCurrentSession().get(SyncClass.class, syncClassId); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncClasses() */ @SuppressWarnings("unchecked") public List<SyncClass> getSyncClasses() throws DAOException { List<SyncClass> classes = (List<SyncClass>) sessionFactory.getCurrentSession().createCriteria(SyncClass.class) .addOrder(Order.asc("name")).list(); if (classes == null && log.isWarnEnabled()) log.warn("getSyncClasses is null."); return classes; } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncClassByName(String) */ public SyncClass getSyncClassByName(String className) throws DAOException { Criteria crit = sessionFactory.getCurrentSession().createCriteria(SyncClass.class) .add(Expression.eq("name", className)); return (SyncClass) crit.uniqueResult(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#deleteOpenmrsObject(org.openmrs.synchronization.OpenmrsObject) */ public void deleteOpenmrsObject(OpenmrsObject o) throws DAOException { sessionFactory.getCurrentSession().delete(o); } /** * Sets hibernate flush mode to org.hibernate.FlushMode.MANUAL. * * @return true if the flush mode was set already manual * @see #isFlushModeManual() * @see org.hibernate.FlushMode * @see org.openmrs.module.sync.api.db.SyncDAO#setFlushModeManual() */ public boolean setFlushModeManual() throws DAOException { boolean isManualAlready = isFlushModeManual(); // don't need to set it if its already manual if (!isManualAlready) sessionFactory.getCurrentSession().setFlushMode(org.hibernate.FlushMode.MANUAL); return isManualAlready; } /** * Sets hibernate flush mode to org.hibernate.FlushMode.AUTO. * * @see org.hibernate.FlushMode * @see org.openmrs.module.sync.api.db.SyncDAO#setFlushModeAutomatic() */ public void setFlushModeAutomatic() throws DAOException { sessionFactory.getCurrentSession().setFlushMode(org.hibernate.FlushMode.AUTO); } /** * Returns true if the flush mode is currently set to manual * * @return true if the flush mode is manual * @throws DAOException */ public boolean isFlushModeManual() throws DAOException { return FlushMode.isManualFlushMode(sessionFactory.getCurrentSession().getFlushMode()); } /** * Executes hibernate flush. * * @see org.hibernate.Session#flush() * @see org.openmrs.module.sync.api.db.SyncDAO#flushSession() */ public void flushSession() throws DAOException { sessionFactory.getCurrentSession().flush(); } /** * Performs generic save of openmrs object using Hibernate session.saveorupdate. * * @throws DAOException */ public void saveOrUpdate(Object object) throws DAOException { sessionFactory.getCurrentSession().saveOrUpdate(object); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncStatistics(java.util.Date, java.util.Date) */ @SuppressWarnings("unchecked") public Map<RemoteServer, LinkedHashSet<SyncStatistic>> getSyncStatistics(Date fromDate, Date toDate) throws DAOException { //first get the list of remote servers and make map out of it List<RemoteServer> servers = this.getRemoteServers(); Map<RemoteServer, LinkedHashSet<SyncStatistic>> map = new HashMap<RemoteServer, LinkedHashSet<SyncStatistic>>(); String hqlChild = "select rs.nickname, ssr.state, count(*) " + "from RemoteServer rs join rs.serverRecords as ssr " + "where rs.serverId = :server_id and ssr.state <> '" + SyncRecordState.NOT_SUPPOSED_TO_SYNC.toString() + "' " + "group by rs.nickname, ssr.state " + "order by nickname, state"; String hqlParent = "select count(*) from SyncRecord where originalUuid = uuid and state <> '" + SyncRecordState.COMMITTED.toString() + "' and state <> '" + SyncRecordState.NOT_SUPPOSED_TO_SYNC.toString() + "'"; //for each server configured, get its stats for (RemoteServer r : servers) { if (r.getServerType() == RemoteServerType.CHILD) { Query q = sessionFactory.getCurrentSession().createQuery(hqlChild); q.setParameter("server_id", r.getServerId()); List<Object[]> rows = q.list(); LinkedHashSet<SyncStatistic> props = new LinkedHashSet<SyncStatistic>(); for (Object[] row : rows) { SyncStatistic stat = new SyncStatistic(SyncStatistic.Type.SYNC_RECORD_COUNT_BY_STATE, row[1].toString(), row[2]); //state/count props.add(stat); } map.put(r, props); } else { //for parent servers, get the number of records in sync record Query q = sessionFactory.getCurrentSession().createQuery(hqlParent); Long count = (Long) q.uniqueResult(); LinkedHashSet<SyncStatistic> props = new LinkedHashSet<SyncStatistic>(); if (count != null) { props.add(new SyncStatistic(SyncStatistic.Type.SYNC_RECORD_COUNT_BY_STATE, "AWAITING", count)); //count } map.put(r, props); } } return map; } /* * called at Openmrs sync parent server: exports the openmrs database to a * DDL output stream for sending it back to a new child node being created */ public void exportChildDB(String uuidForChild, OutputStream os) throws DAOException { PrintStream out = new PrintStream(os); Set<String> tablesToSkip = new HashSet<String>(); { tablesToSkip.add("hl7_in_archive"); tablesToSkip.add("hl7_in_queue"); tablesToSkip.add("hl7_in_error"); tablesToSkip.add("formentry_archive"); tablesToSkip.add("formentry_queue"); tablesToSkip.add("formentry_error"); tablesToSkip.add("sync_class"); tablesToSkip.add("sync_import"); tablesToSkip.add("sync_record"); tablesToSkip.add("sync_server"); tablesToSkip.add("sync_server_class"); tablesToSkip.add("sync_server_record"); // TODO: figure out which other tables to skip // tablesToSkip.add("obs"); // tablesToSkip.add("concept"); // tablesToSkip.add("patient"); } List<String> tablesToDump = new ArrayList<String>(); Session session = sessionFactory.getCurrentSession(); String schema = (String) session.createSQLQuery("SELECT schema()").uniqueResult(); log.warn("schema: " + schema); // Get all tables that we'll need to dump { Query query = session .createSQLQuery("SELECT tabs.table_name FROM INFORMATION_SCHEMA.TABLES tabs WHERE tabs.table_schema = '" + schema + "'"); for (Object tn : query.list()) { String tableName = (String) tn; if (!tablesToSkip.contains(tableName.toLowerCase())) tablesToDump.add(tableName); } } log.warn("tables to dump: " + tablesToDump); String thisServerGuid = getGlobalProperty(SyncConstants.PROPERTY_SERVER_UUID); // Write the DDL Header as mysqldump does { out.println("-- ------------------------------------------------------"); out.println("-- Database dump to create an openmrs child server"); out.println("-- Schema: " + schema); out.println("-- Parent GUID: " + thisServerGuid); out.println("-- Parent version: " + OpenmrsConstants.OPENMRS_VERSION); out.println("-- ------------------------------------------------------"); out.println(""); out.println("/*!40101 SET CHARACTER_SET_CLIENT=utf8 */;"); out.println("/*!40101 SET NAMES utf8 */;"); out.println("/*!40103 SET TIME_ZONE='+00:00' */;"); out.println("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;"); out.println("/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;"); out.println("/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;"); out.println("/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;"); out.println("/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;"); out.println("/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;"); out.println("/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;"); out.println("/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;"); out.println(""); } try { // JDBC way of doing this // Connection conn = // DriverManager.getConnection("jdbc:mysql://localhost/" + schema, // "test", "test"); Connection conn = sessionFactory.getCurrentSession().connection(); try { Statement st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); // Get the create database statement ResultSet rs = st.executeQuery("SHOW CREATE DATABASE " + schema); for (String tableName : tablesToDump) { out.println(); out.println("--"); out.println("-- Table structure for table `" + tableName + "`"); out.println("--"); out.println("DROP TABLE IF EXISTS `" + tableName + "`;"); out.println("SET @saved_cs_client = @@character_set_client;"); out.println("SET character_set_client = utf8;"); rs = st.executeQuery("SHOW CREATE TABLE " + tableName); while (rs.next()) { out.println(rs.getString("Create Table") + ";"); } out.println("SET character_set_client = @saved_cs_client;"); out.println(); { out.println("-- Dumping data for table `" + tableName + "`"); out.println("LOCK TABLES `" + tableName + "` WRITE;"); out.println("/*!40000 ALTER TABLE `" + tableName + "` DISABLE KEYS */;"); boolean first = true; rs = st.executeQuery("select * from " + tableName); ResultSetMetaData md = rs.getMetaData(); int numColumns = md.getColumnCount(); int rowNum = 0; boolean insert = false; while (rs.next()) { if (rowNum == 0) { insert = true; out.print("INSERT INTO `" + tableName + "` VALUES "); } ++rowNum; if (first) { first = false; } else { out.print(", "); } if (rowNum % 20 == 0) { out.println(); } out.print("("); for (int i = 1; i <= numColumns; ++i) { if (i != 1) { out.print(","); } if (rs.getObject(i) == null) { out.print("NULL"); } else { switch (md.getColumnType(i)) { case Types.VARCHAR: case Types.CHAR: case Types.LONGVARCHAR: out.print("'"); out.print(rs.getString(i).replaceAll("\n", "\\\\n").replaceAll("'", "\\\\'")); out.print("'"); break; case Types.BIGINT: case Types.DECIMAL: case Types.NUMERIC: out.print(rs.getBigDecimal(i)); break; case Types.BIT: out.print(rs.getBoolean(i)); break; case Types.INTEGER: case Types.SMALLINT: case Types.TINYINT: out.print(rs.getInt(i)); break; case Types.REAL: case Types.FLOAT: case Types.DOUBLE: out.print(rs.getDouble(i)); break; case Types.BLOB: case Types.VARBINARY: case Types.LONGVARBINARY: Blob blob = rs.getBlob(i); out.print("'"); InputStream in = blob.getBinaryStream(); while (true) { int b = in.read(); if (b < 0) { break; } char c = (char) b; if (c == '\'') { out.print("\'"); } else { out.print(c); } } out.print("'"); break; case Types.CLOB: out.print("'"); out.print(rs.getString(i).replaceAll("\n", "\\\\n").replaceAll("'", "\\\\'")); out.print("'"); break; case Types.DATE: out.print("'" + rs.getDate(i) + "'"); break; case Types.TIMESTAMP: out.print("'" + rs.getTimestamp(i) + "'"); break; default: throw new RuntimeException("TODO: handle type code " + md.getColumnType(i) + " (name " + md.getColumnTypeName(i) + ")"); } } } out.print(")"); } if (insert) { out.println(";"); insert = false; } out.println("/*!40000 ALTER TABLE `" + tableName + "` ENABLE KEYS */;"); out.println("UNLOCK TABLES;"); out.println(); } } } finally { conn.close(); } // Now we mark this as a child out.println("-- Now mark this as a child database"); if (uuidForChild == null) uuidForChild = SyncUtil.generateUuid(); out.println("update global_property set property_value = '" + uuidForChild + "' where property = '" + SyncConstants.PROPERTY_SERVER_UUID + "';"); // Write the footer of the DDL script { out.println("/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;"); out.println("/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;"); out.println("/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;"); out.println("/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;"); out.println("/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;"); out.println("/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;"); out.println("/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;"); out.println("/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;"); } out.flush(); out.close(); } catch (IOException ex) { log.error("IOException", ex); } catch (SQLException ex) { log.error("SQLException", ex); } } /* * Called at Openmrs sync child: imports the DDL backup of the parent server * from an input stream generated from the parent DB @Impl: Reads the DDL * statement line by line and updates the child DB */ public void importParentDB(InputStream in) { try { Connection conn = sessionFactory.getCurrentSession().connection(); Statement statement = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); BufferedReader reader = new BufferedReader(new InputStreamReader(in)); String line; StringBuffer query = new StringBuffer(); boolean queryEnds = false; while ((line = reader.readLine()) != null) { if ((line.startsWith("#") || line.startsWith("--"))) { continue; } queryEnds = line.endsWith(";"); query.append(line); if (queryEnds) { try { statement.execute(query.toString()); } catch (SQLException ex) { log.warn(ex); ex.printStackTrace(); } query.setLength(0); } } in.close(); } catch (IOException ex) { log.warn(ex); ex.printStackTrace(); } catch (SQLException ex) { log.warn(ex); ex.printStackTrace(); } } public void generateDataFile(File outFile, String[] ignoreTables) { // TODO totally breaks if someone isn't using mysql as the backend // TODO get custom location of mysql instead of just relying on path? // TODO make this linux compatible String[] props = getConnectionProperties(); String username = props[0]; String password = props[1]; String database = props[2]; String host = props[3]; String port = props[4]; try { if (!outFile.exists()) outFile.createNewFile(); } catch (IOException io) { log.warn(io.toString()); } List<String> commands = new ArrayList<String>(); commands.add("mysqldump"); commands.add("-u" + username); commands.add("-p" + password); commands.add("-h" + host); commands.add("-P" + port); commands.add("--hex-blob"); commands.add("-q"); commands.add("-e"); commands.add("--single-transaction"); commands.add("-r"); commands.add(outFile.getAbsolutePath()); commands.add(database); // mark the tables to ignore for (String table : ignoreTables) { table = table.trim(); if (StringUtils.hasLength(table)) { commands.add("--ignore-table"); commands.add(database + "." + table); } } String output; if (OpenmrsConstants.UNIX_BASED_OPERATING_SYSTEM) output = execCmd(outFile.getParentFile(), commands.toArray(new String[] {})); else output = execCmd(null, commands.toArray(new String[] {})); if (output != null && output.length() > 0) { log.debug("Exec called: " + Arrays.asList(commands)); log.debug("Output of exec: " + output); } } /** * Auto generated method comment * * @return */ private String[] getConnectionProperties() { Properties props = Context.getRuntimeProperties(); // username, password, database, host, port String[] connProps = { "test", "test", "openmrs", "localhost", "3306" }; String username = (String) props.get("database.username"); if (username == null) username = (String) props.get("connection.username"); if (username != null) connProps[0] = username; String password = (String) props.get("database.password"); if (password == null) password = (String) props.get("connection.password"); if (password != null) connProps[1] = password; // get database name String database = "openmrs"; String host = "localhost"; String port = "3306"; String connectionUrl = (String) props.get("connection.url"); if (connectionUrl == null) connectionUrl = (String) props.get("connection.url"); if (connectionUrl != null) { //jdbc:mysql://localhost:3306/openmrs //jdbc:mysql:mxj://127.0.0.1:3317/openmrs? //Assuming the last full colon will be that before the port int lastColonPos = connectionUrl.lastIndexOf(':'); int slash = connectionUrl.indexOf('/', lastColonPos); int qmark = connectionUrl.indexOf('?', lastColonPos); database = connectionUrl.substring(slash + 1, qmark != -1 ? qmark : connectionUrl.length()); connProps[2] = database; //Assuming that port is explicitly set int doubleSlashPos = connectionUrl.indexOf("://"); host = connectionUrl.substring(doubleSlashPos + 3, lastColonPos); connProps[3] = host; port = connectionUrl.substring(lastColonPos + 1, slash); connProps[4] = port; } return connProps; } /** * @param cmdWithArguments * @param wd * @return */ private String execCmd(File wd, String[] cmdWithArguments) { log.debug("executing command: " + Arrays.toString(cmdWithArguments)); StringBuffer out = new StringBuffer(); try { Process p = (wd != null) ? Runtime.getRuntime().exec(cmdWithArguments, null, wd) : Runtime.getRuntime().exec( cmdWithArguments); out.append("Normal cmd output:\n"); Reader reader = new InputStreamReader(p.getInputStream()); BufferedReader input = new BufferedReader(reader); int readChar = 0; while ((readChar = input.read()) != -1) { out.append((char) readChar); } input.close(); reader.close(); out.append("ErrorStream cmd output:\n"); reader = new InputStreamReader(p.getErrorStream()); input = new BufferedReader(reader); readChar = 0; while ((readChar = input.read()) != -1) { out.append((char) readChar); } input.close(); reader.close(); Integer exitValue = p.waitFor(); log.debug("Process exit value: " + exitValue); } catch (Exception e) { log.error("Error while executing command: '" + cmdWithArguments + "'", e); } log.debug("execCmd output: \n" + out.toString()); return out.toString(); } public void execGeneratedFile(File generatedDataFile) { // TODO this depends on mysql being on the path // TODO fix this so that queries are parsed out and run linebyline? String[] props = getConnectionProperties(); String username = props[0]; String password = props[1]; String database = props[2]; String host = props[3]; String port = props[4]; String path = generatedDataFile.getAbsolutePath(); path = path.replace("\\", "/"); // replace windows file separator with // forward slash String[] commands = { "mysql", "-e", "source " + path, "-f", "-u" + username, "-p" + password, "-h" + host, "-P" + port, "-D" + database }; String output; if (OpenmrsConstants.UNIX_BASED_OPERATING_SYSTEM) output = execCmd(generatedDataFile.getParentFile(), commands); else output = execCmd(null, commands); if (output != null && output.length() > 0) { log.error("Exec call: " + Arrays.asList(commands)); log.error("Output of exec: " + output); } } public <T extends OpenmrsObject> T getOpenmrsObjectByUuid(Class<T> clazz, String uuid) { Criteria crit = sessionFactory.getCurrentSession().createCriteria(clazz); crit.add(Restrictions.eq("uuid", uuid)); return (T) crit.uniqueResult(); } public <T extends OpenmrsObject> T getOpenmrsObjectByPrimaryKey(String classname, Object primaryKey) { Criteria crit; try { crit = sessionFactory.getCurrentSession().createCriteria(Context.loadClass(classname)); crit.add(Restrictions.idEq(primaryKey)); return (T) crit.uniqueResult(); } catch (ClassNotFoundException e) { log.warn("getOpenmrsObjectByPrimaryKey couldn't find class: " + classname, e); return null; } } public <T extends OpenmrsObject> String getUuidForOpenmrsObject(Class<T> clazz, String id) { Criteria crit = sessionFactory.getCurrentSession().createCriteria(clazz); crit.add(Restrictions.idEq(id)); crit.setProjection(Projections.property("uuid")); List<Object[]> rows = crit.list(); Object[] rowOne = rows.get(0); return (String) rowOne[0]; // get the first column of the first row } /** * @see org.openmrs.module.sync.api.db.SyncDAO#processCollection(java.lang.Class, * java.lang.String, java.lang.String) */ public void processCollection(Class collectionType, String incoming, String originalRecordUuid) throws Exception { OpenmrsObject owner = null; String ownerClassName = null; String ownerCollectionPropertyName = null; String ownerUuid = null; String ownerCollectionAction = null; //is this coll update or recreate? NodeList nodes = null; Set entries = null; int i = 0; boolean needsRecreate = false; //first find out what kid of set we are dealing with: //Hibernate PersistentSortedSet == TreeSet, note this is derived from PersistentSet so we have to test for it first //Hibernate PersistentSet == HashSet if (!org.hibernate.collection.PersistentSet.class.isAssignableFrom(collectionType)) { //don't know how to process this collection type log.error("Do not know how to process this collection type: " + collectionType.getName()); throw new SyncIngestException(SyncConstants.ERROR_ITEM_BADXML_MISSING, null, incoming, null); } //next, pull out the owner node and get owner instance: //we need reference to owner object before we start messing with collection entries nodes = SyncUtil.getChildNodes(incoming); if (nodes == null) { throw new SyncIngestException(SyncConstants.ERROR_ITEM_BADXML_MISSING, null, incoming, null); } for (i = 0; i < nodes.getLength(); i++) { if ("owner".equals(nodes.item(i).getNodeName())) { //pull out collection owner info: class name of owner, its uuid, and name of poperty on owner that holds this collection ownerClassName = ((Element) nodes.item(i)).getAttribute("type"); ownerCollectionPropertyName = ((Element) nodes.item(i)).getAttribute("properyName"); ownerCollectionAction = ((Element) nodes.item(i)).getAttribute("action"); ownerUuid = ((Element) nodes.item(i)).getAttribute("uuid"); break; } } if (ownerUuid == null) { log.error("Owner uuid is null while processing collection."); throw new SyncIngestException(SyncConstants.ERROR_ITEM_BADXML_MISSING, null, incoming, null); } owner = (OpenmrsObject) SyncUtil.getOpenmrsObj(ownerClassName, ownerUuid); //we didn't get the owner record: throw an exception //TODO: in future, when we have conflict resolution, this may be handled differently if (owner == null) { log.error("Cannot retrieve the collection's owner object."); log.error("Owner info: " + "\nownerClassName:" + ownerClassName + "\nownerCollectionPropertyName:" + ownerCollectionPropertyName + "\nownerCollectionAction:" + ownerCollectionAction + "\nownerUuid:" + ownerUuid); throw new SyncIngestException(SyncConstants.ERROR_ITEM_BADXML_MISSING, null, incoming, null); } //NOTE: we cannot just new up a collection and assign to parent: //if hibernate mapping has cascade deletes, it will orphan existing collection and hibernate will throw error //to that effect: "A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance" //*only* if this is recreate; clear up the existing collection and start over Method m = null; m = SyncUtil.getGetterMethod(owner.getClass(), ownerCollectionPropertyName); if (m == null) { log.error("Cannot retrieve getter method for ownerCollectionPropertyName:" + ownerCollectionPropertyName); log.error("Owner info: " + "\nownerClassName:" + ownerClassName + "\nownerCollectionPropertyName:" + ownerCollectionPropertyName + "\nownerCollectionAction:" + ownerCollectionAction + "\nownerUuid:" + ownerUuid); throw new SyncIngestException(SyncConstants.ERROR_ITEM_BADXML_MISSING, null, incoming, null); } entries = (Set) m.invoke(owner, (Object[]) null); /*Two instances where even after this we may need to create a new collection: * a) when collection is lazy=false and it is newly created; then asking parent for it will * not return new & empty proxy, it will return null * b) Special recreate logic: * if fetched owner instance has nothing attached, then it is safe to just create brand new collection * and assign it to owner without worrying about getting orphaned deletes error * if owner has something attached, then we process recreate as delete/update; * that is clear out the existing entries and then proceed to add ones received via sync. * This code essentially mimics hibernate org.hibernate.engine.Collections.prepareCollectionForUpdate() * implementation. * NOTE: The unfortunate bi-product of this approach is that this series of events will not produce * 'recreate' event in the interceptor: thus parent's sync journal entries will look slightly diferently * from what child was sending up: child sent up single 'recreate' collection action however * parent will instead have single 'update' with deletes & updates in it. Presumably, this is a distinction * without a difference. */ if (entries == null) { if (org.hibernate.collection.PersistentSortedSet.class.isAssignableFrom(collectionType)) { needsRecreate = true; entries = new TreeSet(); } else if (org.hibernate.collection.PersistentSet.class.isAssignableFrom(collectionType)) { needsRecreate = true; entries = new HashSet(); } } if (entries == null) { log.error("Was not able to retrieve reference to the collection using owner object."); log.error("Owner info: " + "\nownerClassName:" + ownerClassName + "\nownerCollectionPropertyName:" + ownerCollectionPropertyName + "\nownerCollectionAction:" + ownerCollectionAction + "\nownerUuid:" + ownerUuid); throw new SyncIngestException(SyncConstants.ERROR_ITEM_BADXML_MISSING, null, incoming, null); } //clear existing entries before adding new ones: if ("recreate".equals(ownerCollectionAction)) { entries.clear(); } //now, finally process nodes, phew!! for (i = 0; i < nodes.getLength(); i++) { if ("entry".equals(nodes.item(i).getNodeName())) { String entryClassName = ((Element) nodes.item(i)).getAttribute("type"); String entryUuid = ((Element) nodes.item(i)).getAttribute("uuid"); String entryAction = ((Element) nodes.item(i)).getAttribute("action"); Object entry = SyncUtil.getOpenmrsObj(entryClassName, entryUuid); // objects like Privilege, Role, and GlobalProperty might have different // uuids for different objects if (entry == null && SyncUtil.hasNoAutomaticPrimaryKey(entryClassName)) { String key = ((Element) nodes.item(i)).getAttribute("primaryKey"); entry = getOpenmrsObjectByPrimaryKey(entryClassName, key); } if (entry == null) { // blindly ignore this entry if it doesn't exist and we're trying to delete it if (!"delete".equals(entryAction)) { //the object not found: most likely cause here is data collision log.error("Was not able to retrieve reference to the collection entry object by uuid."); log.error("Entry info: " + "\nentryClassName: " + entryClassName + "\nentryUuid: " + entryUuid + "\nentryAction: " + entryAction); log.error("Sync record original uuid: " + originalRecordUuid); throw new SyncIngestException(SyncConstants.ERROR_ITEM_UUID_NOT_FOUND, ownerClassName + " missing " + entryClassName + "," + entryUuid, incoming, null); } } else if ("update".equals(entryAction)) { if (!OpenmrsUtil.collectionContains(entries, entry)) { entries.add(entry); } } else if ("delete".equals(entryAction)) { OpenmrsUtil.collectionContains(entries, entry); if (!entries.remove(entry)) { //couldn't find entry in collection: hmm, bad implementation of equals? //fall back to trying to find the item in entries by uuid OpenmrsObject toBeRemoved = null; for (Object o : entries) { if (o instanceof OpenmrsObject) { if (entryUuid.equals(((OpenmrsObject) o).getUuid())) { toBeRemoved = (OpenmrsObject) o; break; } } } if (toBeRemoved == null) { //the item to be removed was not located in the collection: log it for reference and continue log.warn("Was not able to process collection entry delete."); log.warn("Owner info: " + "\nownerClassName:" + ownerClassName + "\nownerCollectionPropertyName:" + ownerCollectionPropertyName + "\nownerCollectionAction:" + ownerCollectionAction + "\nownerUuid:" + ownerUuid); log.warn("entry info: " + "\nentryClassName:" + entryClassName + "\nentryUuid:" + entryUuid); log.warn("Sync record original uuid: " + originalRecordUuid); } else { //finally, remove it from the collection entries.remove(toBeRemoved); } } } else { log.error("Unknown collection entry action, action was: " + entryAction); throw new SyncIngestException(SyncConstants.ERROR_ITEM_NOT_COMMITTED, ownerClassName, incoming, null); } } } /* * Pass the original uuid to interceptor: this will prevent the change * from being sent back to originating server. */ HibernateSyncInterceptor.setOriginalRecordUuid(originalRecordUuid); //assign collection back to the owner if it is recreated if (needsRecreate) { SyncUtil.setProperty(owner, ownerCollectionPropertyName, entries); } //finally, trigger update try { //no need to mess around with precommit actions for collections, at least //at this point SyncUtil.updateOpenmrsObject(owner, ownerClassName, ownerUuid); } catch (Exception e) { log.error("Unexpected exception occurred while processing hibernate collections", e); throw new SyncIngestException(SyncConstants.ERROR_ITEM_NOT_COMMITTED, ownerClassName, incoming, null); } } /* For full description of how this works read class comments for * {@link SyncSubclassStub}. * * (non-Javadoc) * @see org.openmrs.module.sync.api.db.SyncDAO#processSyncSubclassStub(org.openmrs.module.sync.SyncSubclassStub) */ public void processSyncSubclassStub(SyncSubclassStub stub) { Connection connection = sessionFactory.getCurrentSession().connection(); boolean stubInsertNeeded = false; PreparedStatement ps = null; int internalDatabaseId = 0; // check if there is a row with a matching person record and no patient record try { // becomes something like "select person_id from person where uuid = x" or "select concept_id from concept where uuid = x" ps = connection.prepareStatement("SELECT " + stub.getParentTableId() + " FROM " + stub.getParentTable() + " WHERE uuid = ?"); ps.setString(1, stub.getUuid()); ps.execute(); ResultSet rs = ps.getResultSet(); if (rs.next()) { stubInsertNeeded = true; internalDatabaseId = rs.getInt(stub.getParentTableId()); } else { stubInsertNeeded = false; } //this should get no rows ps = connection.prepareStatement("SELECT " + stub.getSubclassTableId() + " FROM " + stub.getSubclassTable() + " WHERE " + stub.getSubclassTableId() + " = ?"); ps.setInt(1, internalDatabaseId); ps.execute(); if (ps.getResultSet().next()) { stubInsertNeeded = false; } ps.close(); ps = null; } catch (SQLException e) { log.error("Error while trying to see if this person is a patient already (or concept is a concept numeric already)", e); } if (ps != null) { try { ps.close(); } catch (SQLException e) { log.error("Error generated while closing statement", e); } } if (stubInsertNeeded) { try { //insert the stub String sql = "INSERT INTO " + stub.getSubclassTable() + " (" + stub.getSubclassTableId() + "COLUMNNAMEGOESHERE) VALUES (?COLUMNVALUEGOESHERE)"; if (CollectionUtils.isNotEmpty(stub.getRequiredColumnNames()) && CollectionUtils.isNotEmpty(stub.getRequiredColumnValues()) && CollectionUtils.isNotEmpty(stub.getRequiredColumnClasses())) { for (int x = 0; x < stub.getRequiredColumnNames().size(); x++) { String column = stub.getRequiredColumnNames().get(x); sql = sql.replace("COLUMNNAMEGOESHERE", ", " + column + "COLUMNNAMEGOESHERE"); sql = sql.replace("COLUMNVALUEGOESHERE", ", ?COLUMNVALUEGOESHERE"); } } sql = sql.replace("COLUMNNAMEGOESHERE", ""); sql = sql.replace("COLUMNVALUEGOESHERE", ""); ps = connection.prepareStatement(sql); ps.setInt(1, internalDatabaseId); if (CollectionUtils.isNotEmpty(stub.getRequiredColumnNames()) && CollectionUtils.isNotEmpty(stub.getRequiredColumnValues()) && CollectionUtils.isNotEmpty(stub.getRequiredColumnClasses())) { for (int x = 0; x < stub.getRequiredColumnValues().size(); x++) { String value = stub.getRequiredColumnValues().get(x); String className = stub.getRequiredColumnClasses().get(x); Class c; try { c = Context.loadClass(className); ps.setObject(x + 2, SyncUtil.getNormalizer(c).fromString(c, value)); } catch (ClassNotFoundException e) { log.error("Unable to convert classname into a Class object " + className); } } } ps.executeUpdate(); //*and* create sync item for this HibernateSyncInterceptor.addSyncItemForSubclassStub(stub); log.debug("Sync Inserted " + stub.getParentTable() + " Stub for " + stub.getUuid()); } catch (SQLException e) { log.warn("SQL Exception while trying to create a " + stub.getParentTable() + " stub", e); } finally { if (ps != null) { try { ps.close(); } catch (SQLException e) { log.error("Error generated while closing statement", e); } } } } return; } /** * (non-Javadoc) * * @see org.openmrs.module.sync.api.db.SyncDAO#isConceptIdValidForUuid(int, java.lang.String) */ public boolean isConceptIdValidForUuid(Integer conceptId, String uuid) { if (uuid == null || conceptId == null) { return true; } boolean ret = true; //assume all is well until proven otherwise PreparedStatement ps = null; Connection connection = sessionFactory.getCurrentSession().connection(); int foundId = 0; String foundUuid = null; try { ps = connection.prepareStatement("SELECT concept_id, uuid FROM concept WHERE (uuid = ? AND concept_id <> ?)" + "OR (uuid <> ? AND concept_id = ?)"); ps.setString(1, uuid); ps.setInt(2, conceptId); ps.setString(3, uuid); ps.setInt(4, conceptId); ps.execute(); ResultSet rs = ps.getResultSet(); if (rs.next()) { ret = false; foundId = rs.getInt("concept_id"); foundUuid = rs.getString("uuid"); String msg = "Found inconsistent data during concept ingest." + "\nto be added conceptid/uuid are:" + conceptId + "/" + uuid + "\nfound in DB conceptid/uuid:" + foundId + "/" + foundUuid; log.error(msg); } ; ps.close(); ps = null; } catch (SQLException e) { log.error("Error while doing isConceptIdValidForUuid.", e); ret = false; } if (ps != null) { try { ps.close(); } catch (SQLException e) { log.error("Error generated while closing statement", e); } } return ret; } public Long getCountOfSyncRecords(RemoteServer server, Date from, Date to, SyncRecordState... states) { Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class); criteria.setProjection(Projections.rowCount()); if (from != null) criteria.add(Restrictions.gt("timestamp", from)); // greater than if (to != null) criteria.add(Restrictions.le("timestamp", to)); // less-than or equal // only check for matching states if any were passed in if (states != null && states.length > 0) criteria.add(Restrictions.in("state", states)); if (server != null) { criteria.createCriteria("serverRecords", "sr"); criteria.add(Restrictions.in("sr.state", states)); criteria.add(Restrictions.eq("sr.syncServer", server)); } return (Long) criteria.uniqueResult(); } //this is a utility method that i used for Sync-180 //won't hurt to leave it around -- may be useful in the future // MG: I'm commenting this out since the CriteriaLoader method signature has // changed in the most recent version of Hibernate and so this method // now causes a compile issue /** private String getSQL(Criteria crit) { String ret = ""; CriteriaImpl c = (CriteriaImpl) crit; SessionImpl s = (SessionImpl) c.getSession(); SessionFactoryImplementor factory = (SessionFactoryImplementor) s.getSessionFactory(); String[] implementors = factory.getImplementors(c.getEntityOrClassName()); CriteriaLoader loader = new CriteriaLoader((OuterJoinLoadable) factory.getEntityPersister(implementors[0]), factory, c, implementors[0], s.getEnabledFilters()); try { Field f = OuterJoinLoader.class.getDeclaredField("sql"); f.setAccessible(true); String sql = (String) f.get(loader); return sql; } catch (Exception ex) { //pass } return ret; } */ /** * @see org.openmrs.module.sync.api.db.SyncDAO#getOlderSyncRecordInState(org.openmrs.module.sync.SyncRecord, * java.util.EnumSet) */ public SyncRecord getOlderSyncRecordInState(SyncRecord syncRecord, EnumSet<SyncRecordState> states) { Criteria criteria = sessionFactory.getCurrentSession().createCriteria(SyncRecord.class); criteria.createAlias("serverRecords", "serverRecord", Criteria.LEFT_JOIN); criteria.add(Restrictions.le("timestamp", syncRecord.getTimestamp())); // We need to look for a lower recordId since some may have the same timestamp. criteria.add(Restrictions.lt("recordId", syncRecord.getRecordId())); // We need to look for errors in both SyncRecord and SyncRecordServer. criteria.add(Restrictions.or(Restrictions.in("state", states), Restrictions.in("serverRecord.state", states))); criteria.addOrder(Order.desc("timestamp")); criteria.addOrder(Order.desc("recordId")); criteria.setMaxResults(1); return (SyncRecord) criteria.uniqueResult(); } /** * @see org.openmrs.module.sync.api.db.SyncDAO#getSyncServerRecord(java.lang.Integer) */ @Override public SyncServerRecord getSyncServerRecord(Integer syncServerRecordId) throws DAOException { return (SyncServerRecord) sessionFactory.getCurrentSession().get(SyncServerRecord.class, syncServerRecordId); } /** * @see SyncDAO#getMostRecentFullyCommittedRecordId() */ public int getMostRecentFullyCommittedRecordId() { Set<String> committedStates = new HashSet<String>(); for (SyncRecordState s : SyncConstants.SYNC_RECORD_COMMITTED_STATES) { committedStates.add(s.name()); } StringBuilder q = new StringBuilder(); if (getParentServer() != null) { q.append("SELECT record_id FROM sync_record WHERE state IN (:states) ORDER BY record_id DESC"); } else { q.append("SELECT sr1.record_id FROM sync_record sr1 "); q.append("INNER JOIN sync_server_record ssr1 ON sr1.record_id = ssr1.record_id "); q.append("WHERE ssr1.state IN (:states) AND NOT EXISTS ("); q.append(" SELECT sr2.record_id FROM sync_record sr2 "); q.append(" INNER JOIN sync_server_record ssr2 ON sr2.record_id=ssr2.record_id "); q.append(" WHERE sr1.record_id=sr2.record_id AND ssr2.state NOT IN (:states)"); q.append(") ORDER BY sr1.record_id DESC"); } Query allCommittedSSRQuery = sessionFactory.getCurrentSession().createSQLQuery(q.toString()); allCommittedSSRQuery.setParameterList("states", committedStates); allCommittedSSRQuery.setMaxResults(1); Object result = allCommittedSSRQuery.uniqueResult(); if(result != null) { return Integer.parseInt(result.toString()); } return -1; } }